/**
 * 
 */
package com.ofek.eventos.net.lks;

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Ofek
 *
 */
public class ClientDatabaseHandle<ID,T> implements IDistributeableDatabaseHandle<ID,T>{
	protected final IMap<ID,ItemVersion<ID,T>> pendingSyncItems;
	protected final IMap<ID,ItemVersion<ID,T>> syncedItems;
	protected final IMap<ID,ItemVersion<ID,T>> pendingRemoval;
	protected final LastKnownHandler<ID> lastKnownHandler = new LastKnownHandler<>();
	protected final LastKnownHandler<ID> removedLastKnowns = new LastKnownHandler<ID>();
	protected final LastKnownHandler<ID> aliveLastKnowns = new LastKnownHandler<ID>();
	private final ReentrantLock lock = new ReentrantLock();


	public ClientDatabaseHandle(
			final IMap<ID, ItemVersion<ID, T>> pendingSyncItems,
			final IMap<ID, ItemVersion<ID, T>> syncedItems,
			final IMap<ID, ItemVersion<ID, T>> pendingRemoval) {
		this.pendingSyncItems = pendingSyncItems;
		this.syncedItems = syncedItems;
		this.pendingRemoval = pendingRemoval;
	}

	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.ocsf.client.IDatabaseHandle#lock()
	 */
	@Override
	public void lock() {
		lock.lock();
	}

	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.ocsf.client.IDatabaseHandle#unlock()
	 */
	@Override
	public void unlock() {
		lock.unlock();
	}

	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.IDatabaseHandle#getLastKnowns()
	 */
	@Override
	public LastKnownHandler<ID> getLastKnowns() {
		return lastKnownHandler;
	}

	/** 
	 * 
	 * new items got from another node are already synced so they go straight to the syncedItems
	 * 
	 * @see com.ofek.eventos.net.lks.IDatabaseHandle#syncItems(java.util.ArrayList)
	 * 
	 */



	/** 
	 * 
	 * 
	 * first phase in a sync iteration - given sync object you need to return all the items that are not known to the user who sent that sync
	 * 
	 * @see com.ofek.eventos.net.lks.IDatabaseHandle#getPendings(com.ofek.eventos.net.lks.LastKnownHandler)
	 */
	@Override
	public ArrayList<ItemVersion<ID, T>> getPendings(final Sync<ID, T> sync) {
		final LastKnownHandler<ID> lastKnowns = sync.lastKnowns;

		final ArrayList<ItemVersion<ID, T>> res = new ArrayList<ItemVersion<ID,T>>();
		final ArrayList<ItemVersion<ID, T>> waitingItems = new ArrayList<ItemVersion<ID,T>>();

		for ( final ItemVersion<ID,T> item : pendingSyncItems.values()  ) 
			if (lastKnowns.isKnown(item.id, item.version)) {
				waitingItems.add(item);
			} else {
				res.add(item);
			}
		for (final ItemVersion<ID, T> item : waitingItems) {
			syncedItems.put(item.id,pendingSyncItems.remove(item.id));
			fireItemSynced(item);
		}
		waitingItems.clear();


		for ( final ItemVersion<ID,T> item : pendingRemoval.values()  ) 
			if (sync.lastKnowns.isKnown(item.id, item.version)) {
				waitingItems.add(item);
			} else {
				res.add(item);
			}
		for (final ItemVersion<ID, T> item : waitingItems) {
			syncedItems.remove(item.id);
			pendingRemoval.remove(item.id);
			removedLastKnowns.forget(item.id);
			lastKnownHandler.forget(item.id);
			fireItemRemoved(item);
		}

		return res;
	}



	/**
	 * second phase of a sync iteration - you are being given all the new items not known to you by the given sync object, you are returning your last knowns for another sync iteration
	 */
	@Override
	public LastKnownHandler<ID> syncItems(final Sync<ID, T> sync) {
		if ( sync.newItems==null ) return lastKnownHandler;
		final ArrayList<ItemVersion<ID, T>> newItems = sync.newItems;
		for ( final ItemVersion<ID,T> item : newItems   ) {
			if ( item.item!=null ) {
				add(item);
			} else {
				remove(item);
			}
		}
		return lastKnownHandler;
	}

	/**
	 * @param item
	 */
	protected void remove(final ItemVersion<ID, T> item) {
		syncedItems.remove(item.id);
		pendingRemoval.put(item.id, item);
		aliveLastKnowns.forget(item.id);
		removedLastKnowns.updateIfNeeded(item.id, item.version);
		lastKnownHandler.updateIfNeeded(item.id, item.version);
	}

	/**
	 * @param item
	 */
	protected void add(final ItemVersion<ID, T> item) {
		syncedItems.put(item.id, item);
		aliveLastKnowns.updateIfNeeded(item.id, item.version);
		lastKnownHandler.updateIfNeeded(item.id, item.version);
		fireItemSynced(item);
	}


	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.IDistributedDatabaseHandle#getVersion(java.lang.Object)
	 */
	@Override
	public Long getVersion(final ID id) {
		ItemVersion<ID, T> maxVersion = maxVersion( pendingSyncItems.get(id),syncedItems.get(id) ); 
		maxVersion = maxVersion(maxVersion,pendingRemoval.get(id));
		return maxVersion==null ? null : maxVersion.version;
	}

	/**
	 * @param itemVersion
	 * @param itemVersion2
	 * @return
	 */
	private ItemVersion<ID, T> maxVersion(final ItemVersion<ID, T> req,final ItemVersion<ID, T> con) {
		if ( req==null && con==null ) return null;
		if ( req==null ) return con;
		if ( con==null ) return req;
		final long reqVH = req.version;
		final long conVH = con.version;
		return reqVH > conVH ? req : con;
	}

	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.IDistributedDatabaseHandle#addRequest(java.lang.Object, java.lang.Object, com.ofek.eventos.net.lks.ItemVersionHandler)
	 */
	@Override
	public void addChangePending(final ID id, final T item, final long version) {
		if ( item==null ) {
			pendingRemoval.put(id,new ItemVersion<ID,T>(id,item,version));
			removedLastKnowns.updateIfNeeded(id, version);
		}
		else {
			pendingSyncItems.put(id, new ItemVersion<ID,T>(id,item,version));
			aliveLastKnowns.updateIfNeeded(id, version);
		}
		lastKnownHandler.updateIfNeeded(id, version);
	}


	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "\nPendingSync "+pendingSyncItems + "\nSynced"+syncedItems+"\nPendingRemoval" + pendingRemoval;
	}

	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.IDistributedDatabaseHandle#getAllSynced()
	 */
	@Override
	public ArrayList<ItemVersion<ID, T>> getAllSynced() {
		return fromIterator(syncedItems.values());
	}
	/**
	 * @param allPending
	 * @return
	 */
	private ArrayList<ItemVersion<ID, T>> fromIterator(
			final Iterable<ItemVersion<ID, T>> iterable) {
		final ArrayList<ItemVersion<ID, T>> arrayList = new ArrayList<>();
		for (final ItemVersion<ID, T> i : iterable) {
			arrayList.add(i);
		}
		return arrayList;
	}
	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.IDistributedDatabaseHandle#getAllPending()
	 */
	@Override
	public ArrayList<ItemVersion<ID, T>> getAllPending() {
		return fromIterator(pendingSyncItems.values());
	}

	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.IDistributeableDatabaseHandle#close()
	 */
	@Override
	public void close() {
	}

	private final ArrayList<DistributedDatabaseListener<ID, T>> listeners = new ArrayList<DistributedDatabaseListener<ID, T>>();
	/* (non-Javadoc)
	 * @see com.ofek.eventos.net.lks.ClientDatabaseHandle#addDistributedDatabaseListener(com.ofek.eventos.net.lks.DistributedDatabaseListener)
	 */
	@Override
	public boolean addDistributedDatabaseListener(final DistributedDatabaseListener<ID, T> l) {
		return listeners.contains(l) ? false : listeners.add(l);

	}


	@Override
	public boolean removeDistributedDatabaseListener(final DistributedDatabaseListener<ID, T> o) {
		return listeners.remove(o);
	}

	/**
	 * @param item
	 */
	protected void fireItemRemoved(final ItemVersion<ID, T> item) {
		for ( final DistributedDatabaseListener<ID, T> l : listeners) 
			l.onItemRemoved(item);
	}

	/**
	 * @param item
	 */
	protected void fireItemSynced(final ItemVersion<ID, T> item) {
		for ( final DistributedDatabaseListener<ID, T> l : listeners) 
			l.onItemSynced(item);
	}

}
